建立你自己的完全链上 DAO 来投资 NFT
为您的 NFT 持有者构建 DAO
什么是 DAO?
DAO 代表去中心化自治组织。您可以将 DAO 视为类似于现实世界中的公司。从本质上讲,DAO 允许成员创建治理决策并对其进行投票。
在传统公司中,当需要做出决定时,公司的董事会或高 管负责做出该决定。然而,在 DAO 中,这个过程是民主化的,任何成员都可以创建提案,所有其他成员都可以对其进行投票。创建的每个提案都有一个投票截止日期,在截止日期之后做出有利于投票结果的决定(是或否)。
DAO 的成员资格通常受到 ERC20 代币所有权或 NFT 所有权的限制。成员资格和投票权与您拥有的代币数量成正比的 DAO 示例包括 Uniswap 和 ENS。基于 NFT 的 DAO 示例包括 Meebits DAO。
构建我们的 DAO
你想为你的 CryptoDevs NFT 的持有者启动一个 DAO。从通过 ICO 获得的 ETH 中,你建立了一个 DAO 库。 DAO 现在有很多 ETH,但目前什么也没做。
您希望允许您的 NFT 持有者创建并投票使用该 ETH 从 NFT 市场购买其他 NFT 的提案,并推测价格。也许将来当你卖回 NFT 时,你会将利润分配给 DAO 的所有成员。
要求
- 任何拥有 CryptoDevs NFT 的人都可以创建从 NFT 市场购买不同 NFT 的提案
- 每个拥有 CryptoDevs NFT 的人都可以投票支持或反对活跃的提案
- 每个 NFT 计为每个提案的一票
- 投票者不能对具有相同 NFT 的同一个提案多次投票
- 如果在截止日期前多数选民投票支持该提案,NFT 购买将自动执行
我们将做什么
- 为了能够在提案通过时自动购买 NFT,您需要一个可以调用 purchase() 函数的链上 NFT 市场。那里有很多 NFT 市场,但为了避免过于复杂,我们将为本教程创建一个简化的假 NFT 市场,因为重点是 DAO。
- 我们还将使用 Hardhat 制作实际的 DAO 智能合约。
- 我们将使用 Next.js 制作网站,以允许用户创建和对提案进行投票
先决条件
- 你已经完成了之前的 NFT Collection 教程。
- 你必须有一些 ETH 给 DAO 财政部
构建
智能合约开发
我们将从创建智能合约开始。我们将制作两个智能合约:
FakeNFTMarketplace.sol
CryptoDevsDAO.sol
为此,我们将使用我们在过去几个教程中一直使用的 Hardhat 开发框架。
为这个项目创建一个名为 DAO-Tutorial 的文件夹,并在该文件夹中打开一个终端窗口。
通过在终端中运行以下命令来设置新的安全帽项目:
mkdir hardhat-tutorial
cd hardhat-tutorial
npm init --yes
npm install --save-dev hardhat
现在您已经安装了 Hardhat,我们可以设置一个项目。在终端中执行以下命令。
在安装 Hardhat 的同一目录中运行:
npx hardhat
确保选择创建 Javascript 项目,然后按照终端中的步骤完成安全帽设置。
现在,让我们从 NPM 安装 @openzeppelin/contracts 包,因为我们将使用 OpenZeppelin 的 Ownable Contract 作为 DAO 合约。
npm install @openzeppelin/contracts
首先,让我们做一个简单的 Fake NFT Marketplace。在 hardhat-tutorial 中的 contracts 目录下创建一个名为 FakeNFTMarketplace.sol 的文件,并添加以下代码。
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
contract FakeNFTMarketplace {
/// @dev Maintain a mapping of Fake TokenID to Owner addresses
mapping(uint256 => address) public tokens;
/// @dev Set the purchase price for each Fake NFT
uint256 nftPrice = 0.1 ether;
/// @dev purchase() accepts ETH and marks the owner of the given tokenId as the caller address
/// @param _tokenId - the fake NFT token Id to purchase
function purchase(uint256 _tokenId) external payable {
require(msg.value == nftPrice, "This NFT costs 0.1 ether");
tokens[_tokenId] = msg.sender;
}
/// @dev getPrice() returns the price of one NFT
function getPrice() external view returns (uint256) {
return nftPrice;
}
/// @dev available() checks whether the given tokenId has already been sold or not
/// @param _tokenId - the tokenId to check for
function available(uint256 _tokenId) external view returns (bool) {
// address(0) = 0x0000000000000000000000000000000000000000
// This is the default value for addresses in Solidity
if (tokens[_tokenId] == address(0)) {
return true;
}
return false;
}
}
FakeNFTMarketplace 公开了一些基本功能,如果提案获得通过,我们将从 DAO 合约中使用这些功能来购买 NFT。 真正的 NFT 市场会更加复杂——因为并非所有 NFT 的价格都相同。
在开始编写 DAO 合约之前,让我们确保一切都编译好。 在终端的 hardhat-tutorial 文件夹中运行以下命令。
npx hardhat compile
并确保没有编译错误。
现在,我们将开始编写 CryptoDevsDAO 合约。 由于这主要是一个完全自定义的合约,并且比我们目前所做的相对复杂,所以我们将一点一点地解释这一点。 首先,让我们为合约编写样板代码。 在 hardhat-tutorial 的 contracts 目录下创建一个名为 CryptoDevsDAO.sol 的新文件,并将以下代码添加到其中。
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
import "@openzeppelin/contracts/access/Ownable.sol";
// We will add the Interfaces here
contract CryptoDevsDAO is Ownable {
// We will write contract code here
}
现在,我们需要调用 FakeNFTMarketplace 合约以及您之前部署的 CryptoDevs NFT 合约的函数。 回想一下高级 Solidity 主题教程,我们需要为这些合约提供一个 接口,因此该合约知道哪些函数可以调用,它们将什么作为参数以及它们返回什么。
通过添加以下代码将以下两个接口添加到您的代码中
/**
* Interface for the FakeNFTMarketplace
*/
interface IFakeNFTMarketplace {
/// @dev getPrice() returns the price of an NFT from the FakeNFTMarketplace
/// @return Returns the price in Wei for an NFT
function getPrice() external view returns (uint256);
/// @dev available() returns whether or not the given _tokenId has already been purchased
/// @return Returns a boolean value - true if available, false if not
function available(uint256 _tokenId) external view returns (bool);
/// @dev purchase() purchases an NFT from the FakeNFTMarketplace
/// @param _tokenId - the fake NFT tokenID to purchase
function purchase(uint256 _tokenId) external payable;
}
/**
* Minimal interface for CryptoDevsNFT containing only two functions
* that we are interested in
*/
interface ICryptoDevsNFT {
/// @dev balanceOf returns the number of NFTs owned by the given address
/// @param owner - address to fetch number of NFTs for
/// @return Returns the number of NFTs owned
function balanceOf(address owner) external view returns (uint256);
/// @dev tokenOfOwnerByIndex returns a tokenID at given index for owner
/// @param owner - address to fetch the NFT TokenID for
/// @param index - index of NFT in owned tokens array to fetch
/// @return Returns the TokenID of the NFT
function tokenOfOwnerByIndex(address owner, uint256 index)
external
view
returns (uint256);
}
现在,让我们考虑一下我们在 DAO 合约中需要哪些功能。
- 以合约状态存储已创建的提案
- 允许 CryptoDevs NFT 的持有者创建 新提案
- 允许 CryptoDevs NFT 的持有者对提案进行投票,因为他们尚未投票,并且提案尚未通过其截止日期
- 允许 CryptoDevs NFT 的持有者在超过截止日期后执行提案,以在提案通过时触发 NFT 购买
让我们从创建一个表示提案的结构开始。 在您的合约中,添加以下代码:
// Create a struct named Proposal containing all relevant information
struct Proposal {
// nftTokenId - the tokenID of the NFT to purchase from FakeNFTMarketplace if the proposal passes
uint256 nftTokenId;
// deadline - the UNIX timestamp until which this proposal is active. Proposal can be executed after the deadline has been exceeded.
uint256 deadline;
// yayVotes - number of yay votes for this proposal
uint256 yayVotes;
// nayVotes - number of nay votes for this proposal
uint256 nayVotes;
// executed - whether or not this proposal has been executed yet. Cannot be executed before the deadline has been exceeded.
bool executed;
// voters - a mapping of CryptoDevsNFT tokenIDs to booleans indicating whether that NFT has already been used to cast a vote or not
mapping(uint256 => bool) voters;
}
让我们还创建一个从提案 ID 到提案的映射以保存所有创建的提案,以及一个计数器来计算存在的提案数量。
// Create a mapping of ID to Proposal
mapping(uint256 => Proposal) public proposals;
// Number of proposals that have been created
uint256 public numProposals;
现在,由于我们将调用 FakeNFTMarketplace 和 CryptoDevsNFT 合约上的函数,让我们为这些合约初始化变量。
IFakeNFTMarketplace nftMarketplace;
ICryptoDevsNFT cryptoDevsNFT;
创建一个构造函数来初始化这些合约变量,并接受部署者的 ETH 存款以填充 DAO ETH 库。 (在后台,由于我们导入了 Ownable 合约,这也会将合约部署者设置为该合约的所有者)
// Create a payable constructor which initializes the contract
// instances for FakeNFTMarketplace and CryptoDevsNFT
// The payable allows this constructor to accept an ETH deposit when it is being deployed
constructor(address _nftMarketplace, address _cryptoDevsNFT) payable {
nftMarketplace = IFakeNFTMarketplace(_nftMarketplace);
cryptoDevsNFT = ICryptoDevsNFT(_cryptoDevsNFT);
}
现在,由于我们希望几乎所有其他函数只能由拥有 CryptoDevs NFT 合约的 NFT 的人调用,因此我们将创建一个修饰符以避免重复代码。
// Create a modifier which only allows a function to be
// called by someone who owns at least 1 CryptoDevsNFT
modifier nftHolderOnly() {
require(cryptoDevsNFT.balanceOf(msg.sender) > 0, "NOT_A_DAO_MEMBER");
_;
}
现在,要对提案进行投票,我们要添加一个额外的限制,即被投票的提案不得超过其截止日期。 为此,我们将创建第二个修饰符。
注意这个修饰符是如何接受参数的!
此外,由于投票只能是两个值之一(是或否) - 我们可以创建一个表示可能选项的枚举。
编写 voteOnProposal 函数
/// @dev voteOnProposal allows a CryptoDevsNFT holder to cast their vote on an active proposal
/// @param proposalIndex - the index of the proposal to vote on in the proposals array
/// @param vote - the type of vote they want to cast
function voteOnProposal(uint256 proposalIndex, Vote vote)
external
nftHolderOnly
activeProposalOnly(proposalIndex)
{
Proposal storage proposal = proposals[proposalIndex];
uint256 voterNFTBalance = cryptoDevsNFT.balanceOf(msg.sender);
uint256 numVotes = 0;
// Calculate how many NFTs are owned by the voter
// that haven't already been used for voting on this proposal
for (uint256 i = 0; i < voterNFTBalance; i++) {
uint256 tokenId = cryptoDevsNFT.tokenOfOwnerByIndex(msg.sender, i);
if (proposal.voters[tokenId] == false) {
numVotes++;
proposal.voters[tokenId] = true;
}
}
require(numVotes > 0, "ALREADY_VOTED");
if (vote == Vote.YAY) {
proposal.yayVotes += numVotes;
} else {
proposal.nayVotes += numVotes;
}
}
我们快完成了! 要执行已超过截止日期的提案,我们将创建最终修改器。
// Create a modifier which only allows a function to be
// called if the given proposals' deadline HAS been exceeded
// and if the proposal has not yet been executed
modifier inactiveProposalOnly(uint256 proposalIndex) {
require(
proposals[proposalIndex].deadline <= block.timestamp,
"DEADLINE_NOT_EXCEEDED"
);
require(
proposals[proposalIndex].executed == false,
"PROPOSAL_ALREADY_EXECUTED"
);
_;
}
注意这个修饰符也需要一个参数!
让我们编写 executeProposal 的代码
/// @dev executeProposal allows any CryptoDevsNFT holder to execute a proposal after it's deadline has been exceeded
/// @param proposalIndex - the index of the proposal to execute in the proposals array
function executeProposal(uint256 proposalIndex)
external
nftHolderOnly
inactiveProposalOnly(proposalIndex)
{
Proposal storage proposal = proposals[proposalIndex];
// If the proposal has more YAY votes than NAY votes
// purchase the NFT from the FakeNFTMarketplace
if (proposal.yayVotes > proposal.nayVotes) {
uint256 nftPrice = nftMarketplace.getPrice();
require(address(this).balance >= nftPrice, "NOT_ENOUGH_FUNDS");
nftMarketplace.purchase{value: nftPrice}(proposal.nftTokenId);
}
proposal.executed = true;
}
至此,我们已经实现了所有核心功能。 但是,我们可以并且应该实现一些附加功能。
- 如果需要,允许合约所有者从 DAO 中提取 ETH
- 允许合约接受更多的 ETH 存款
我们继承的 Ownable 合约包含一个修饰符 onlyOwner,它将函数限制为只能由合约所有者调用。 让我们使用该修饰符来实现 withdrawEther。
/// @dev withdrawEther allows the contract owner (deployer) to withdraw the ETH from the contract
function withdrawEther() external onlyOwner {
payable(owner()).transfer(address(this).balance);
}